package com.fowo.api.common.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ModelBuildEventListener;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.read.metadata.holder.ReadHolder;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.alibaba.excel.read.metadata.holder.ReadSheetHolder;
import com.alibaba.excel.read.metadata.holder.ReadWorkbookHolder;
import com.alibaba.excel.read.processor.AnalysisEventProcessor;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.fowo.api.common.model.ImportRow;
import com.fowo.api.common.util.ListUtils;
import com.fowo.api.sys.entity.SysImportTemplate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * 基于系统导入模版配置的 Excel 模型处理器
 * @author yl_ls
 */
@Slf4j
public abstract class TemplateModelBuildEventListener<T, CT> extends ModelBuildEventListener {
    private static final HashMap<String, SysImportTemplate> DEFAULT_TEMPLATES = new HashMap<>();
    private final Class<T> modelClass;
    private final Class<CT> childModelClass;
    private final Field childField;
    private SysImportTemplate importTemplate;
    private HashMap<Integer, List<MappingTarget>> columnIndexTargetMap;
    private boolean useChildModel = false;
    // 最后导入的主数据实例
    private T lastImportPo;
    // 最后导入的主数据上下文
    private ProxyAnalysisContext lastAnalysisContext;
    // 最后导入的子数据列表实例
    private List<ImportRow<CT>> lastChild;
    protected boolean allowBadMainChildImport;
    // 所有主数据，在 allowBadMainChildImport 并且 useChildModel 时保存所有主导入行，在提交时一起处理
    private List<T> allImportPoList;
    // 所有主数据的行代理，数量始终与 allImportPoList 对应
    private List<ProxyAnalysisContext> allImportProxyAnalysisContexts;

    public TemplateModelBuildEventListener(Class<T> modelClass, SysImportTemplate importTemplate) {
        this(modelClass, null, importTemplate);
    }

    public TemplateModelBuildEventListener(Class<T> modelClass, Class<CT> childModelClass, SysImportTemplate importTemplate) {
        this.modelClass = modelClass;
        this.childModelClass = childModelClass;
        this.importTemplate = importTemplate;
        if (childModelClass != null) {
            for (Field field : this.modelClass.getDeclaredFields()) {
                if (field.isAnnotationPresent(ChildExcelProperty.class)) {
                    this.childField = field;
                    this.childField.setAccessible(true);
                    return;
                }
            }
        }
        this.childField = null;
    }

    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> cellDataMap, AnalysisContext context) {
        if (importTemplate == null) {
            importTemplate = getDefaultTemplate();
        }
        if (childModelClass != null) {
            testIsEnableChildModel(cellDataMap, importTemplate);
        }
        HashMap<Integer, List<MappingTarget>> columnIndexTargetMap = new HashMap<>();
        for (Map.Entry<Integer, ReadCellData<?>> headCellDataEntry : cellDataMap.entrySet()) {
            String headText = headCellDataEntry.getValue().getStringValue();
            foundMapping(headText, columnIndexTargetMap, headCellDataEntry.getKey());
        }
        this.columnIndexTargetMap = columnIndexTargetMap;
        if (allowBadMainChildImport && useChildModel) {
            allImportPoList = new ArrayList<>();
            allImportProxyAnalysisContexts = new ArrayList<>();
        }
        super.invokeHead(cellDataMap, context);
    }

    // 测试导入 Excel 中是否有子表字段
    private void testIsEnableChildModel(Map<Integer, ReadCellData<?>> cellDataMap, SysImportTemplate importTemplate) {
        this.useChildModel = importTemplate.getSetting().getMappings().stream().anyMatch(
                m -> m.getField().contains(".") &&
                        textInCell(cellDataMap, m.getExcelColumnNames())
        );
    }

    private boolean textInCell(Map<Integer, ReadCellData<?>> cellDataMap, List<String> texts) {
        return cellDataMap.values().stream().anyMatch(
                cell -> cell.getType() == CellDataTypeEnum.STRING &&
                        cell.getStringValue() != null &&
                        (
                                // 兼容可能后加的必填标题
                                texts.contains(cell.getStringValue().trim()) ||
                                texts.contains("*" + cell.getStringValue().trim())
                        )
        );
    }

    private SysImportTemplate getDefaultTemplate() {
        return DEFAULT_TEMPLATES.computeIfAbsent(modelClass.getName(), cn -> this.createDefaultTemplate());
    }

    private SysImportTemplate createDefaultTemplate() {
        SysImportTemplate.Setting setting = new SysImportTemplate.Setting();
        List<SysImportTemplate.Mapping> mappings = new ArrayList<>();
        if (childField != null) {
            ReflectionUtils.doWithFields(childModelClass, inChildField ->
                fieldToMappings(mappings, inChildField, childField.getName() + "." + inChildField.getName())
            );
        }
        ReflectionUtils.doWithFields(modelClass, field -> {
            if (field == childField) {
                return;
            }
            fieldToMappings(mappings, field, field.getName());
        });
        setting.setMappings(mappings);
        SysImportTemplate defaultTemplate = new SysImportTemplate();
        defaultTemplate.setSetting(setting);
        return defaultTemplate;
    }

    private static void fieldToMappings(List<SysImportTemplate.Mapping> mappings, Field field, String fieldName) {
        final ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
        if (annotation == null) {
            return;
        }
        SysImportTemplate.Mapping mapping = new SysImportTemplate.Mapping();
        mapping.setField(fieldName);
        mapping.setExcelColumnNames(Collections.singletonList(annotation.value()[0]));
        mappings.add(mapping);
    }

    private void foundMapping(String headText, HashMap<Integer, List<MappingTarget>> columnIndexTargetMap, Integer cellColumnIndex) {
        final List<SysImportTemplate.Mapping> mappings = this.importTemplate.getSetting().getMappings();
        for(int i = 0;i < mappings.size(); i++) {
            final SysImportTemplate.Mapping mapping = mappings.get(i);
            for (int ic = 0; ic < mapping.getExcelColumnNames().size(); ic++) {
                if (headText.equals(mapping.getExcelColumnNames().get(ic))) {
                    final List<MappingTarget> mappingTargets = columnIndexTargetMap.computeIfAbsent(cellColumnIndex, (i2) -> new ArrayList<>());
                    mappingTargets.add(new MappingTarget(ic, i));
                }
            }
        }
    }

    @Override
    public void invoke(Map<Integer, ReadCellData<?>> cellDataMap, AnalysisContext context) {
        if (cellDataMap.values().stream().allMatch(c -> c.getType() == CellDataTypeEnum.EMPTY || (c.getType() == CellDataTypeEnum.STRING && !StringUtils.hasText(c.getStringValue())))) {
            // 跳过空数据行
            return;
        }

        try {
            Map<String, String[]> rowValues = new HashMap<>();
            for (Map.Entry<Integer, List<MappingTarget>> integerListEntry : columnIndexTargetMap.entrySet()) {
                final ReadCellData<?> cellData = cellDataMap.get(integerListEntry.getKey());
                if (cellData == null) {
                    continue;
                }
                for (MappingTarget mappingTarget : integerListEntry.getValue()) {
                    final SysImportTemplate.Mapping mapping = this.importTemplate.getSetting().getMappings().get(mappingTarget.mappingIndex);
                    String[] fieldValues =  rowValues.computeIfAbsent(mapping.getField(), fn -> new String[mapping.getExcelColumnNames().size()]);
                    if (cellData.getType() == CellDataTypeEnum.NUMBER) {
                        fieldValues[mappingTarget.concatIndex] = cellData.getNumberValue().toString();
                    } else if (cellData.getType() == CellDataTypeEnum.BOOLEAN) {
                        fieldValues[mappingTarget.concatIndex] = Boolean.TRUE.equals(cellData.getBooleanValue()) ? "是" : "否";
                    } else {
                        fieldValues[mappingTarget.concatIndex] = cellData.getStringValue();
                    }
                }
            }
            if (useChildModel) {
                childRowHandle(context, rowValues);
            } else {
                invokeRow(rowHandle(rowValues), context);
            }
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (allImportPoList != null) {
            log.debug("开始为无序主子表进行批量导入 {} 个", allImportPoList.size());
            for (int i = 0; i < allImportPoList.size(); i++) {
                invokeRow(allImportPoList.get(i), allImportProxyAnalysisContexts.get(i));
            }
        }
        else if (useChildModel && lastImportPo != null) {
            invokeRow(lastImportPo, lastAnalysisContext);
        }
        super.doAfterAllAnalysed(context);
    }

    private void childRowHandle(AnalysisContext context, Map<String, String[]> rowValues) throws ReflectiveOperationException {
        // 主数据处理
        T row = rowHandle(rowValues);
        if (allImportPoList != null) {
            T parentRow = findParentRow(row);
            if (parentRow == null) {
                lastImportPo = row;
                lastAnalysisContext = new ProxyAnalysisContext(context);
                lastAnalysisContext.keepRowHolder();
                lastChild = new ArrayList<>();
                childField.set(lastImportPo, lastChild);
                allImportPoList.add(row);
                allImportProxyAnalysisContexts.add(lastAnalysisContext);
            } else {
                lastChild = (List<ImportRow<CT>>) childField.get(parentRow);
            }
        } else {
            boolean isSomeParent = isSomeParent(row);
            if (!isSomeParent) {
                if (lastImportPo != null) {
                    // 提交上一组数据
                    invokeRow(lastImportPo, lastAnalysisContext);
                }
                lastImportPo = row;
                lastAnalysisContext = new ProxyAnalysisContext(context);
                lastAnalysisContext.keepRowHolder();
                lastChild = new ArrayList<>();
                childField.set(lastImportPo, lastChild);
            }
        }
        // 子数据处理
        CT childRow = childModelClass.getConstructor().newInstance();
        for (Map.Entry<String, String[]> fieldValues : rowValues.entrySet()) {
            if (!fieldValues.getKey().contains(".")) {
                continue;
            }
            final Field childField = ReflectionUtils.findField(childModelClass, fieldValues.getKey().split("\\.")[1]);
            String value = concat(fieldValues.getValue());
            if (childField != null && value != null) {
                if (!childField.canAccess(childRow)) {
                    childField.setAccessible(true);
                }
                ReflectionUtils.setField(childField, childRow, value);
            }
        }
        ImportRow<CT> newImportChildRow = new ImportRow<>();
        newImportChildRow.setRow(childRow);
        newImportChildRow.setRowNumber(context.readRowHolder().getRowIndex() + 1);
        lastChild.add(newImportChildRow);
    }

    // 判断此数据是否为新的父级
    private boolean isSomeParent(T row) throws ReflectiveOperationException {
        ParentIdentification parentIdentification = childModelClass.getAnnotation(ParentIdentification.class);
        if (parentIdentification != null) {
            return isEquals(row, lastImportPo, parentIdentification.value());
        }
        throw new RuntimeException("无法处理父子表同时导入时的唯一性，没有配置导入使用的唯一识别逻辑分组");
    }

    private T findParentRow(T row) throws ReflectiveOperationException {
        ParentIdentification parentIdentification = childModelClass.getAnnotation(ParentIdentification.class);
        if (parentIdentification != null) {
            for (T parentRow : allImportPoList) {
                if(isEquals(row, parentRow, parentIdentification.value())) {
                    return parentRow;
                }
            }
            return null;
        }
        throw new RuntimeException("无法处理父子表同时导入时的唯一性，没有配置导入使用的唯一识别逻辑分组");
    }

    private boolean isEquals(Object a, Object b, String...props) throws ReflectiveOperationException {
        if (a == null || b == null) {
            return false;
        }
        for(String prop : props) {
            final Field aField = a.getClass().getDeclaredField(prop);
            final Field bField = b.getClass().getDeclaredField(prop);
            aField.setAccessible(true);
            bField.setAccessible(true);
            Object aValue = aField.get(a);
            Object bValue = bField.get(b);
            if (!Objects.equals(aValue, bValue)) {
                return false;
            }
        }
        return true;
    }

    private T rowHandle(Map<String, String[]> rowValues) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        T row = modelClass.getConstructor().newInstance();
        for (Map.Entry<String, String[]> fieldValues : rowValues.entrySet()) {
            final Field field = ReflectionUtils.findField(modelClass, fieldValues.getKey());
            String value = concat(fieldValues.getValue());
            if (field != null && value != null) {
                if (!field.canAccess(row)) {
                    field.setAccessible(true);
                }
                ReflectionUtils.setField(field, row, value);
            }
        }
        return row;
    }

    private String concat(String[] values) {
        boolean hasValue = false;
        StringBuilder sb = new StringBuilder();
        for (String value : values) {
            if (value != null) {
                sb.append(value);
                hasValue = true;
            }
        }
        if (!hasValue) {
            return null;
        }
        return sb.toString();
    }

    public abstract void invokeRow(T row, AnalysisContext context);

    @AllArgsConstructor
    static class MappingTarget {
        int concatIndex;
        int mappingIndex;
    }

    @SuppressWarnings("deprecation")
    static class ProxyAnalysisContext implements AnalysisContext {
        private final AnalysisContext context;
        private ReadRowHolder keepRowHolder;

        public ProxyAnalysisContext(AnalysisContext context) {
            this.context = context;
        }

        @Override
        public void currentSheet(ReadSheet readSheet) {
            context.currentSheet(readSheet);
        }

        @Override
        public ReadWorkbookHolder readWorkbookHolder() {
            return context.readWorkbookHolder();
        }

        @Override
        public ReadSheetHolder readSheetHolder() {
            return context.readSheetHolder();
        }

        @Override
        public void readRowHolder(ReadRowHolder readRowHolder) {
            context.readRowHolder(readRowHolder);
        }

        @Override
        public ReadRowHolder readRowHolder() {
            if (keepRowHolder != null) {
                return keepRowHolder;
            }
            return context.readRowHolder();
        }

        @Override
        public ReadHolder currentReadHolder() {
            return context.currentReadHolder();
        }

        @Override
        public Object getCustom() {
            return context.getCustom();
        }

        @Override
        public AnalysisEventProcessor analysisEventProcessor() {
            return context.analysisEventProcessor();
        }

        @Override
        public List<ReadSheet> readSheetList() {
            return context.readSheetList();
        }

        @Override
        public void readSheetList(List<ReadSheet> readSheetList) {
            context.readSheetList(readSheetList);
        }

        @Override
        public ExcelTypeEnum getExcelType() {
            return context.getExcelType();
        }

        @Override
        public InputStream getInputStream() {
            return context.getInputStream();
        }

        @Override
        public Integer getCurrentRowNum() {
            return context.getCurrentRowNum();
        }

        @Override
        public Integer getTotalCount() {
            return context.getTotalCount();
        }

        @Override
        public Object getCurrentRowAnalysisResult() {
            return context.getCurrentRowAnalysisResult();
        }

        @Override
        public void interrupt() {
            context.interrupt();
        }

        public void keepRowHolder() {
            this.keepRowHolder = context.readRowHolder();
        }
    }
}
